<?php
/**
 * This file defines the {@see \BF\Validation\IValidator} interface.
 *
 * @author     Kado <ulfikado@gmail.com>
 * @category   Beluga PHP5.3+ Framework
 * @package    BF\Validation
 * @since      2015-01-31 00:29
 * @subpackage Core
 * @version    0.1
 */

namespace BF\Validation
{

   /**
    * Each validator must implement this abstract class, to be usable as a validator (no sanitizer).
    *
    * You have only to implement the __validate() method.
    *
    * @property boolean $AllowEmpty    Are emty values allowed for a valid request? (default=FALSE)
    * @property boolean $Required      Defines if the associated value must be defined, to say a valid request exists. (default=TRUE)
    * @property string  $DefaultValue  This value is used if there is no require for the field to be defined for a
    *                                  request, and the field does not exist. (default=undefined)
    * @property string  $RequiredValue If the field value can only use one single value to say its a usable request,
    *                                  you can define here the required string value. (default=undefined)
    * @property string  $DisplayName   The (localized) display name of the field.
    * @since    v0.1
    */
   abstract class Validator
   {

      # <editor-fold defaultstate="collapsed" desc=" - - -   P R O T E C T E D   F I E L D S   - - - - - - - - - - - - - - - - - - - -">

      /**
       * This array defines all named options of the validator. THe options are accessible by the methods and accessors
       * defined by the {@see \ArrayAccess}, {@see \Countable} and {@see \Traversable} implementations.
       *
       * @var array
       */
      protected $_options;

      /**
       * The value to validate or NULL if no value exists
       *
       * @var string|NULL
       */
      protected $_value;
      protected $_trimValue;

      /**
       * It contains the type of the used input source to getting the validated value, after validate(…) is finished.
       *
       * It can use a value defined by the following constants:
       *
       * - {@see \INPUT_GET}: Using data from GET requests.
       * - {@see \INPUT_POST}: Using data from POST requests.
       * - {@see \INPUT_COOKIE}: Using data from cookies.
       * - {@see \INPUT_SESSION}: Using SESSION data.
       * - {@see \INPUT_SERVER}: Using data defined by serv…
       * - {@see \INPUT_ENV}: Using data, defined by environment.
       * - {@see \INPUT_REQUEST}: Using data from GET or POST requests.
       * - {@see \INPUT_CUSTOM}: Using data, defined by array, passed to the {@see ::setCustomData()} method.
       *
       * @var integer
       */
      protected $_inputType;

      /**
       * The name of the validated value. It's only defined after a validate(…) call.
       *
       * @var string
       */
      protected $_key;

      /**
       * The custom source defines a source array for getting the value to validate. The value can be
       * defined from outside the class by using {@see ::setCustomData()}. Its only used if {@see ::$_inputType}
       * uses the {@see \INPUT_CUSTOM} value (defined by {@see ::validate()}.
       *
       * @var array
       */
      protected $_data;

      /**
       * Defines, if after the call of ::validate() the value can be used to say, this is a usable request.
       *
       * @var boolean
       */
      protected $_isRequest;

      /**
       * This field contains the string, defining the last error message after calling the ::validate() method.
       *
       * @var string|NULL
       */
      protected $_message;

      public $LastResult = false;

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - >   P R O T E C T E D   C O N S T R U C T O R   - - - - - - - - - - - - - - -">

      /**
       * Inits a new instance.
       *
       * @param array $options Associative options array
       */
      protected function __construct( array $options = array() )
      {

         $this->_options   = array();
         $this->_isRequest = false;

         self::setBooleanOption( 'AllowEmpty',    $options, false );
         self::setBooleanOption( 'Required',      $options, true );
         self::setStringOption ( 'DisplayName',   $options, 'Undefined' );
         self::setBooleanOption( 'RequiredValue', $options, null );

         $this->_options = $options;

         $this->_data = array();

      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - -   P U B L I C   M E T H O D S   - - - - - - - - - - - - - - - - - - - - - -">

      /**
       * It does the validation and sanitize the checked value.
       *
       * Usable $inputType values are:
       *
       * - {@see \INPUT_GET}: Using data from GET requests.
       * - {@see \INPUT_POST}: Using data from POST requests.
       * - {@see \INPUT_COOKIE}: Using data from cookies.
       * - {@see \INPUT_SESSION}: Using SESSION data.
       * - {@see \INPUT_SERVER}: Using data defined by serv…
       * - {@see \INPUT_ENV}: Using data, defined by environment.
       * - {@see \INPUT_REQUEST}: Using data from GET or POST requests.
       * - {@see \INPUT_CUSTOM}: Using data, defined by array, passed to the {@see ::setCustomData()} method.
       *
       * After the validation you can found the unsanitized value by getValue() method.
       *
       * @param  integer        $inputType Where to get the input data array from, containing the value to check.
       *                                   Usable values are defined by
       * @param  string|integer $key       The key to get
       * @param  string|null    $displayName
       * @return boolean                   Returns TRUE if validation is successfull, FALSE otherwise.
       * @throws \BF\ArgumentException
       */
      public function validate( $inputType, $key, $displayName = null )
      {

         // Initialize the method depending instance fields.
         $this->_inputType   = $inputType;
         if ( ! empty( $displayName ) )
         {
            $this->_options[ 'DisplayName' ] = $displayName;
         }
         if ( empty( $this->_options[ 'DisplayName' ] ) )
         {
            $this->_options[ 'DisplayName' ] = $key;
         }
         $this->_key         = $key;
         $this->_value       = null;
         $this->_isRequest   = false;
         $this->_message     = null;

         switch ( $inputType )
         {
            case \INPUT_POST:
            case \INPUT_GET:
            case \INPUT_COOKIE:
            case \INPUT_ENV:
            case \INPUT_SERVER:
               if ( \filter_has_var( $inputType, $key ) )
               {
                  $this->_value = \filter_var( $key, \FILTER_DEFAULT );
               }
               break;
            case \INPUT_CUSTOM:
               if ( isset( $this->_data[ $key ] ) )
               {
                  $this->_value = $this->_data[ $key ];
               }
               break;
            case \INPUT_SESSION:
               if ( isset( $_SESSION[ $key ] ) )
               {
                  $this->_value = $_SESSION[ $key ];
               }
               break;
            case \INPUT_REQUEST:
               if ( isset( $_REQUEST[ $key ] ) )
               {
                  $this->_value = $_REQUEST[ $key ];
               }
               break;
            default:
               if ( ! $this->_options[ 'Required' ] && isset( $this->_options[ 'DefaultValue' ] ) )
               {
                  $this->_value = \strval( $this->_options[ 'DefaultValue' ] );
                  break;
               }
               throw new \BF\ArgumentException(
                  'inputType', $inputType,
                  'A unknown input source type is used. Please use one of the INPUT_* constants to solve this error.',
                  \E_USER_ERROR
               );
         }

         // First we need to handle NULL values. It means the field is undefined.
         if ( \is_null( $this->_value ) )
         {

            if ( $this->_options[ 'Required' ] )
            {

               // Field is undefined, and undefined fields results in a "This is not a Request"
               $this->_message   = \BF\_3(
                  '_Validation',
                  'NO REQUEST: The required %s value "%s" from %s source is undefined.',
                  $this->_options[ 'DisplayName' ],
                  $this->_key,
                  self::GetInputTypeName( $inputType )
               );

               // In this case we left  $this->_isRequest = false;  unchanged and return FALSE
               return $this->__setLastResult( false );

            }

            // Now (undefined field but valid request) we assign the initial required options, if they are undefined

            if ( isset( $this->_options[ 'DefaultValue' ] ) )
            {
               // A default value is defined for this case. Use it as current value and make sure it is a string
               $this->_value     = \strval( $this->_options[ 'DefaultValue' ] );
               $this->_trimValue = \trim( $this->_value );
            }
            else
            {
               // We should use a last fallback: empty string.
               $this->_value     = '';
               $this->_trimValue = $this->_value;
            }
         }
         else
         {
            $this->_trimValue = \trim( $this->_value );
         }

         // Now its sure its a request depending to this field only.
         $this->_isRequest = true;

         if ( \strlen( $this->_trimValue ) < 1 )
         {

            // The value is EMPTY.

            if ( ! $this->_options[ 'AllowEmpty' ] )
            {

               // Empty values will result in a invalid request.
               // Yes the field uses a empty value (Not allowed)
               $this->_message   = \BF\_1(
                  '_Validation',
                  'The %s field can not use a empty value!',
                  $this->_options[ 'DisplayName' ]
               );

               // We simple return FALSE here
               return $this->__setLastResult( false );

            }

            if ( isset( $this->_options[ 'RequiredValue' ] ) &&
               ( $this->_options[ 'RequiredValue' ] !== $this->_value ) )
            {

               // A special value is the exclusice allowed value, to say it is a usable request
               // But this empty string is not the allowed value.
               $this->_isRequest = false;
               $this->_message   = \BF\_3(
                  '_Validation',
                  'NO REQUEST: The required %s value "%s" from %s source is dont matches the required value.',
                  $this->_options[ 'DisplayName' ],
                  $this->_key,
                  self::GetInputTypeName( $inputType )
               );
               return $this->__setLastResult( false );

            }

            // Value is EMPTY and its not required to be valid only if it's not empty. No other checks are required!
            return $this->__setLastResult( true );

         }

         // We are sure, the value is not empty. We are doing the implementation depending validation.
         return $this->__validate();

      }

      /**
       * The magic getter for read accessing the validator options by dynamic properties.
       *
       * @param  string $name The name of the required validator option (NOT caseless!)
       * @return mixed        Returns the requested Option, boolean FALSE if the option is unknown.
       */
      public final function __get( $name )
      {

         return $this->getOption( $name );

      }

      /**
       * Let you set all validator options by the dynamic property way.
       *
       * @param  string $name  The name of the property to set.
       * @param  mixed  $value The value to set.
       */
      public final function __set( $name, $value )
      {

         $this->setOption( $name, $value );

      }

      /**
       * Returns if a option with defined name has a usable value (!== NULL)
       *
       * @param  string $name The Option name
       * @return boolean
       */
      public final function __isset( $name )
      {
         return isset( $this->_options[ $name ] );
      }

      /**
       * Returns if a option with defined name is usable with this validator.
       *
       * @param  string $name The Option name
       * @return boolean.
       */
      public final function hasOption( $name )
      {
         return \array_key_exists( $name, $this->_options );
      }

      /**
       * Returns the value of the validator option. If the option is undefined the defined default value is set
       * for this options, and is also returned.
       *
       * @param string $optionName   The name of the required validator option (NOT caseless!)
       * @param mixed  $defaultValue This value is used if the option dont exists. If so, the option becomes this value
       *                             and is returned.
       * @return mixed               Returns the value, or $defaultValue if the options dont exists on call.
       */
      public function getOption( $optionName, $defaultValue = false )
      {
         if ( empty( $optionName ) || ! \is_string( $optionName ) )
         {
            return $defaultValue;
         }
         if ( ! \array_key_exists( $optionName, $this->_options ) )
         {
            $this->_options[ $optionName ] = $defaultValue;
         }
         return $this->_options[ $optionName ];
      }

      /**
       * Returns the unsanitized value after a successed or failed validation.
       *
       * @return string Returns unsanitized value.
       */
      public function getValue()
      {
         return $this->_value;
      }

      /**
       * Sets the value of a validator option. If you want to check the option values, changed here, you must overwrite
       * this method. But do not forget to call parent::setOption(…), if youre option name(s) dont match!
       *
       * All options, used here, are also avialable by array accessor $validator[ 'OptionName' ] and as dynamic
       * properties like: $validator->OptionName.
       *
       * @param  string $optionName The name of the option to set.
       * @param  mixed  $value      The option value to set.
       */
      public function setOption( $optionName, $value )
      {
         $tmp = null;
         switch ( $optionName )
         {
            case 'AllowEmpty':
            case 'Required':
               if ( \BF\TypeTool::IsBoolConvertible( $value, $tmp ) )
               {
                  $this->_options[ $optionName ] = $tmp;
               }
               $this->LastResult = null;
               break;
            case 'DefaultValue':
            case 'RequiredValue':
               if ( \BF\TypeTool::IsStringConvertible( $value, $tmp ) )
               {
                  $this->_options[ $optionName ] = $tmp;
               }
               $this->LastResult = null;
               break;
            case 'DisplayName':
               if ( \BF\TypeTool::IsStringConvertible( $value, $tmp ) )
               {
                  if ( \strlen( \trim( $tmp  ) ) > 0 )
                  {
                     $this->_options[ $optionName ] = $tmp;
                  }
               }
               $this->LastResult = null;
               break;
            default:
               $this->_options[ $optionName ] = $value;
               $this->LastResult = null;
               break;
         }
      }

      /**
       * Sets the custom data array, used as source if validate() is called with a input type INPUT_CUSTOM.
       *
       * @param array $customData
       */
      public function setCustomData( array $customData = array() )
      {
         $this->_data = $customData;
      }

      /**
       * Count elements of an object.
       *
       * @return integer The custom count as an integer.
       * @link           http://php.net/manual/en/countable.count.php
       */
      public function count()
      {
         return \count( $this->_options );
      }

      /**
       * Returns, after the call of ::validate(), if the checked value is defined like, to say its a usable request
       * for defined field, or not.
       *
       * @return boolean
       */
      public function isRequest()
      {
         return $this->_isRequest;
      }

      /**
       * Returns the last thrown error message while executing ::validate(…).
       *
       * @return string|null
       */
      public function getMessage()
      {
         return $this->_message;
      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - -   P R O T E C T E D   A B S T R A C T   M E T H O D S   - - - - - - - - - -">

      /**
       * Extending classes have to define this method for doing the validation.
       *
       * Required data are defined by $this->_options, $this->_value, $this->_inputType, $this->_key
       *
       * The defined $this->_value is always more than a empty string. If its empty the depending checks are run
       * inside ::validate() before this method is called.
       *
       * @return boolean It must return TRUE if validation is successful, FALSE if validation fails, or if the field is
       *                 undefined, what results in "The is no request here". If so, ::isRequest() informs you.
       */
      protected abstract function __validate();

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - -   P R O T E C T E D   M E T H O D S   - - - - - - - - - - - - - - - - - - -">

      /**
       * @param  $result
       * @return boolean
       */
      protected function __setLastResult( $result )
      {
         $this->LastResult = $result;
         return $this->LastResult;
      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - -   P U B L I C   S T A T I C   M E T H O D S   - - - - - - - - - - - - - - -">

      /**
       * Returns the Name, associated with the defined input type, defined by the global INPUT_* constants.
       *
       * @param  integer $inputType INPUT_GET, INPUT_POST, INPUT_COOKIE, etc.
       * @return string             Returns the Name like 'GET', 'POST', 'COOKIE', etc.
       */
      public static function GetInputTypeName( $inputType )
      {
         switch ( $inputType )
         {
            case \INPUT_POST:
               return 'POST';
            case \INPUT_GET:
               return 'GET';
            case \INPUT_COOKIE:
               return 'COOKIE';
            case \INPUT_SESSION:
               return 'SESSION';
            case \INPUT_SERVER:
               return 'SERVER';
            case \INPUT_ENV:
               return 'ENVIRONMENT';
            case \INPUT_REQUEST:
               return \BF\_0( '_Validation', 'GET or POST' );
            case \INPUT_CUSTOM:
               return \BF\_0( '_Validation', 'CUSTOM' );
            default:
         }
         return '';
      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - -   P R O T E C T E D   S T A T I C   M E T H O D S   - - - - - - - - - - - -">

      /**
       * Sets a boolean Option with defined name, if its currently undefined by $options.
       *
       * @param  string  $name     The Name of the option
       * @param  array   $options  The options array reference. Its contains after the method call the required option.
       * @param  boolean $defaultValue The default value to set, if the option does not exist. (default=FALSE)
       * @param  boolean $nullable Is NULL a usable value of this option? (default=FALSE)
       * @return void
       */
      protected static function setBooleanOption( $name, array &$options, $defaultValue = false, $nullable = false )
      {
         if ( ! \array_key_exists( $name, $options ) )
         {
            $options[ $name ] = $defaultValue;
         }
         else
         {
            if ( $nullable && \is_null( $options[ $name ] ) )
            {
               return;
            }
            $tmpRef = null;
            if ( ! \BF\TypeTool::IsBoolConvertible( $options[ $name ], $tmpRef ) )
            {
               $options[ $name ] = $defaultValue;
            }
            else
            {
               $options[ $name ] = $tmpRef;
            }
         }
      }

      /**
       * Sets a array Option with defined name, if its currently undefined by $options.
       *
       * @param  string  $name     The Name of the option
       * @param  array   $options  The options array reference. Its contains after the method call the required option.
       * @param  array   $defaultValue The default value to set, if the option does not exist. (default=array())
       * @return void
       */
      protected static function setArrayOption( $name, array &$options, array $defaultValue = array() )
      {
         if ( ! isset( $options[ $name ] ) || ! \is_array( $options[ $name ] ) )
         {
            $options[ $name ] = $defaultValue;
         }
         else
         {
            $options[ $name ] = $options[ $name ];
         }
      }

      /**
       * Sets a string Option with defined name, if its currently undefined by $options.
       *
       * @param  string  $name     The Name of the option
       * @param  array   $options  The options array reference. Its contains after the method call the required option.
       * @param  string  $defaultValue The default value to set, if the option does not exist. (default='')
       * @param  boolean $nullable Is NULL a usable value of this option? (default=FALSE)
       * @return void
       */
      protected static function setStringOption( $name, array &$options, $defaultValue = '', $nullable = false )
      {
         $tmpRef = null;
         if ( ! \array_key_exists( $name, $options ) )
         {
            $options[ $name ] = $defaultValue;
         }
         else
         {
            if ( $nullable && \is_null( $options[ $name ] ) )
            {
               return;
            }
            if ( ! \BF\TypeTool::IsStringConvertible( $options[ $name ], $tmpRef ) )
            {
               $options[ $name ] = $defaultValue;
            }
            else
            {
               $options[ $name ] = $tmpRef;
            }
         }
      }

      /**
       * Sets a integer Option with defined name, if its currently undefined by $options.
       *
       * @param  string  $name     The Name of the option
       * @param  array   $options  The options array reference. Its contains after the method call the required option.
       * @param  integer $defaultValue The default value to set, if the option does not exist. (default=0)
       * @param  boolean $nullable Is NULL a usable value of this option? (default=FALSE)
       * @return void
       */
      protected static function setIntegerOption( $name, array &$options, $defaultValue = 0, $nullable = false )
      {
         if ( ! \array_key_exists( $name, $options ) )
         {
            $options[ $name ] = $defaultValue;
         }
         else
         {
            if ( $nullable && \is_null( $options[ $name ] ) )
            {
               return;
            }
            if ( ! \BF\TypeTool::IsInteger( $options[ $name ] ) )
            {
               $options[ $name ] = $defaultValue;
            }
            else
            {
               $options[ $name ] = \intval( $options[ $name ] );
            }
         }
      }

      # </editor-fold>

   }

}

